/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.template;

import cn.webwheel.compiler.JavaC;
import cn.webwheel.el.*;
import cn.webwheel.template.plain.PlainElement;
import cn.webwheel.template.plain.PlainNode;
import cn.webwheel.template.plain.PlainTemplateParser;
import cn.webwheel.template.plain.PlainText;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 模板组件渲染器的生成类。
 */
public class ComponentRendererBuilder<T> {

    private static String TN_W = "w:";
    private static String TN_Q = "q:";
    private static String TN_V = "v:";

    static {
        String s = System.getProperty("webwheel.template.tn_w");
        if (s != null) {
            TN_W = s;
        }
        s = System.getProperty("webwheel.template.tn_q");
        if (s != null) {
            TN_Q = s;
        }
        s = System.getProperty("webwheel.template.tn_v");
        if (s != null) {
            TN_V = s;
        }
    }

    private static int seq;

    private Class<T> componentClass;
    private List<PlainNode> rootContent;

    private CodeWriter codeWriter;

    private Stack<Var> comContexts = new Stack<Var>();
    private Stack<VarContext> varContexts = new Stack<VarContext>();

    /**
     * 构造方法。
     *
     * @param componentClass 组件类的类型
     * @param template       模板文本
     * @throws PlainTemplateParser.TemplateParserException
     *          模板解析异常
     */
    public ComponentRendererBuilder(Class<T> componentClass, String template) throws PlainTemplateParser.TemplateParserException {
        this.componentClass = componentClass;
        this.rootContent = new PlainTemplateParser(template).parse();
        comContexts.push(new ComponentContext(new GenericClass(componentClass), "b0"));
        varContexts.push(new DomVarContext("this", "b0", new GenericClass(componentClass)));
    }

    /**
     * 生成模板组件渲染器类，此类为动态生成，可使用{@link Class#newInstance()}进行实例化。
     *
     * @return 模板组件渲染器类
     */
    @SuppressWarnings("unchecked")
    public Class<? extends ComponentRenderer<T>> build() {

        if (codeWriter != null) throw new IllegalStateException();

        codeWriter = new CodeWriter();

        String name = "ComponentRenderer_" + componentClass.getSimpleName() + "_" + seq++;

        if (JavaC.javassist) {
            code("public class " + name + " implements " + ComponentRenderer.class.getName() + " {");
            code("public void render(java.io.Writer pw, Object j_b0, " + ComponentRenderer.class.getName() + " delegate) throws java.io.IOException {");
            code(componentClass.getCanonicalName() + " b0 = (" + componentClass.getCanonicalName() + ")j_b0;");
        } else {
            code("public class " + name + " implements " + ComponentRenderer.class.getName() + "<" + componentClass.getCanonicalName() + "> {");
            code("public void render(java.io.Writer pw, " + componentClass.getCanonicalName() + " b0, " + ComponentRenderer.class.getName() + " delegate) throws java.io.IOException {");
        }
        genContent(rootContent);
        code("}");
        code("}");
        return JavaC.getInst(componentClass.getClassLoader()).compile(name, codeWriter.toString());
    }

    private void code(String s) {
        codeWriter.code(s);
    }

    private void print(String s) {
        codeWriter.print(s);
    }

    private static class ComExp {
        Expression exp;
        boolean collection;
        boolean outerComponent;
    }

    private ComExp getComExp(PlainElement e, boolean remove) {
        String v = e.getAttributes().get(TN_W);
        if (v == null) {
            return null;
        }
        ComExp comExp = new ComExp();

        boolean nonCollection = false;
        if (v.startsWith("-")) {
            nonCollection = true;
            v = v.substring(1);
        }
        if (v.startsWith("*")) {
            comExp.outerComponent = true;
            v = v.substring(1);
        }
        comExp.exp = parseExpression(v);
        if (comExp.exp == null || comExp.exp.getReturnType().isPrimitive()) return null;

        if (!nonCollection)
            comExp.collection = comExp.exp.getReturnType().isCollection();
        if (!comExp.outerComponent && e.getAttributes().size() == 1) {
            comExp.outerComponent = e.getContent() == null ||
                    e.getContent().isEmpty() ||
                    (e.getContent().size() == 1 && e.getContent().get(0) instanceof PlainText && e.getContent().get(0).toString().trim().isEmpty());
        }

        if (remove) e.getAttributes().remove(TN_W);
        return comExp;
    }

    private void genContent(List<PlainNode> content) {
        for (int i = 0; i < content.size(); i++) {
            PlainNode node = content.get(i);
            if (node instanceof PlainElement) {
                PlainElement e = (PlainElement) node;
                //remove collection pseudo elements
                ComExp comExp = getComExp(e, false);
                String gap = "";
                boolean gapvalid = false;
                if (comExp != null && comExp.collection && i < content.size() - 1) {
                    boolean textbegin = true;
                    for (int lasti = ++i; i < content.size(); i++) {
                        PlainNode pn = content.get(i);
                        if (pn instanceof PlainElement) {
                            ComExp ce = getComExp((PlainElement) pn, false);
                            if (ce == null || !ce.collection || !ce.exp.equals(comExp.exp)) {
                                if (!textbegin) {
                                    i = lasti;
                                }
                                break;
                            }
                            textbegin = true;
                            gapvalid = true;
                        } else {
                            if (textbegin) {
                                textbegin = false;
                                lasti = i;
                            }
                            if (i == content.size() - 1) {
                                i = lasti;
                                break;
                            }
                            if (!gapvalid) {
                                gap += pn.toString();
                            }
                        }
                    }
                    i--;
                }
                if (!gapvalid || gap.isEmpty()) gap = null;
                gen(e, gap);
            } else {
                gen(node.toString(), false);
            }
        }
    }

    private void gen(PlainElement element, String gap) {
        ComExp comExp = getComExp(element, true);
        if (comExp != null) {
            String ivar = null;
            String bvar = "b" + comContexts.size();
            if (!comExp.collection) {
                code("{");
                declareVar(comExp.exp, bvar);
                comContexts.push(new ComponentContext(comExp.exp.getReturnType(), bvar));
                DomVarContext varCtx = new DomVarContext();
                varCtx.add("this", bvar, comExp.exp.getReturnType());
                varContexts.push(varCtx);
            } else {
                String var = declareVar(comExp.exp, null);
                code("if(" + var + "!=null) {");
                ivar = "i" + comContexts.size();
                code("int " + ivar + " = 0;");
                if (comExp.exp.getReturnType().toClass().isArray()) {
                    code("for(;" + ivar + "<" + var + ".length;" + ivar + "++) {");
                    code(comExp.exp.getReturnType().getComponentType() + " " + bvar + " = " + var + "[" + ivar + "];");
                } else {
                    String itvar = var;
                    code("for(");
                    if (!Iterator.class.isAssignableFrom(comExp.exp.getReturnType().toClass())) {
                        itvar = "it" + comContexts.size();
                        code("java.util.Iterator " + itvar + "=" + var + ".iterator()");
                    }
                    code(";" + itvar + ".hasNext();" + ivar + "++) {");
                    code(comExp.exp.getReturnType().getComponentType() + " " + bvar + " = (" + comExp.exp.getReturnType().getComponentType() + ")" + itvar + ".next();");
                }
                comContexts.push(new ComponentContext(comExp.exp.getReturnType().getComponentType(), bvar));
                DomVarContext varCtx = new DomVarContext();
                varCtx.add("this", bvar, comExp.exp.getReturnType().getComponentType());
                varCtx.add("_idx", ivar, new GenericClass(int.class));
                varContexts.push(varCtx);
            }
            if (comExp.outerComponent) {
                code("if(" + bvar + "!=null && delegate!=null) {");
            } else {
                code("if(" + bvar + "!=null) {");
            }
            if (gap != null && ivar != null) {
                Var com = comContexts.pop();
                VarContext var = varContexts.pop();
                code("if(" + ivar + "!=0) {");
                gen(gap, false);
                code("}");
                comContexts.push(com);
                varContexts.push(var);
            }
            if (comExp.outerComponent) {
                code("delegate.render(pw, " + bvar + ", delegate);");
            } else {
                genVar(element);
            }
            varContexts.pop();
            comContexts.pop();
            if (comExp.collection) {
                code("}");
            }
            code("}}");
            return;
        }
        genVar(element);
    }

    private void genVar(PlainElement element) {
        Map<String, Expression> map = null;
        for (Map.Entry<String, String> entry : element.getAttributes().entrySet()) {
            if (!entry.getKey().startsWith(TN_V)) continue;
            if (entry.getKey().length() <= TN_V.length()) continue;
            String name = entry.getKey().substring(TN_V.length());
            if (!name.matches("^[a-zA-Z\\$_][a-zA-Z0-9\\$_]*$")) continue;
            Expression exp = parseExpression(entry.getValue());
            if (exp == null) {
                exp = parseExpression("'" + entry.getValue() + "'");
                if (exp == null) continue;
            }
            if (map == null) {
                map = new HashMap<String, Expression>();
            }
            map.put(name, exp);
        }
        if (map != null) {
            code("{");
            DomVarContext varContext = new DomVarContext();
            for (Map.Entry<String, Expression> entry : map.entrySet()) {
                element.getAttributes().remove(TN_V + entry.getKey());
                String var = declareVar(entry.getValue(), null);
                varContext.add(entry.getKey(), var, entry.getValue().getReturnType());
            }
            varContexts.push(varContext);
            gen2(element);
            varContexts.pop();
            code("}");
        } else {
            gen2(element);
        }
    }

    private String beforeIf(Expression exp) {
        String var = declareVar(exp, null);
        beforeIf(exp, var);
        return var;
    }

    private void beforeIf(Expression exp, String var) {
        if (exp.getReturnType().toClass() == Boolean.class) {
            code("if(" + var + "!=null && " + var + ") {");
        } else if (exp.getReturnType().toClass() == boolean.class) {
            code("if(" + var + ") {");
        } else {
            code("if(" + var + "!=" + exp.getReturnType().getDefaultValue() + ") {");
        }
    }

    private void gen2(PlainElement element) {
        String s = element.getAttributes().get(TN_Q);
        if (s != null) {
            Expression exp = parseExpression(s);
            if (exp != null) {

                element.getAttributes().remove(TN_Q);

                if (exp.getEvalString().equals("false")) return;

                if (!exp.getEvalString().equals("true")) {
                    beforeIf(exp);
                    gen3(element);
                    code("}");
                    return;
                }
            }
        }
        gen3(element);
    }

    private void gen3(PlainElement element) {
        Expression tagexp = null;
        String tagvar = null;
        String s = element.getAttributes().get(TN_W + "tag");
        if (s != null) {
            tagexp = parseExpression(s);
            element.getAttributes().remove(TN_W + "tag");

            if (tagexp != null) {

                tagvar = "v" + seq++;

                code(tagexp.getReturnType() + " " + tagvar + " = " + tagexp.getReturnType().getDefaultValue() + ";");
            } else {
                element.setTag(s);
            }
        }
        s = element.getAttributes().get(TN_Q + "tag");
        if (s != null) {
            Expression exp = parseExpression(s);
            if (exp != null) {

                element.getAttributes().remove(TN_Q + "tag");

                if (exp.getEvalString().equals("false")) {
                    genContent(element);
                    return;
                }

                if (!exp.getEvalString().equals("true")) {
                    String var = beforeIf(exp);
                    genTagOpen(element, tagvar, tagexp);
                    code("}");
                    if (isEmptyTag(element)) return;
                    genContent(element);
                    beforeIf(exp, var);
                    genTagClose(element, tagvar, tagexp);
                    code("}");
                    return;
                }
            }
        }
        genTagOpen(element, tagvar, tagexp);
        if (isEmptyTag(element)) return;
        genContent(element);
        genTagClose(element, tagvar, tagexp);
    }

    private void genContent(PlainElement element) {
        String s = element.getAttributes().get(TN_Q + "content");
        if (s != null) {
            Expression exp = parseExpression(s);
            if (exp != null) {

                element.getAttributes().remove(TN_Q + "content");

                if (exp.getEvalString().equals("false")) return;

                if (!exp.getEvalString().equals("true")) {
                    beforeIf(exp);
                    genContent2(element);
                    code("}");
                    return;
                }
            }
        }
        genContent2(element);
    }

    private void genContent2(PlainElement element) {
        String s = element.getAttributes().get(TN_W + "content");
        if (s != null) {

            element.getAttributes().remove(TN_W + "content");

            gen(s, true);

            return;
        }
        genContent(element.getContent());
    }

    private void writeTag(PlainElement element, String tagvar, Expression tagexp) {
        if (tagvar != null) {
            if (tagexp.getReturnType().isPrimitive()) {
                code("pw.write(String.valueOf(" + tagvar + "));");
            } else {
                code("if(" + tagvar + "!=null) {");
                if (tagexp.getReturnType().toClass() == String.class) {
                    code("pw.write(" + tagvar + ");");
                } else {
                    code("pw.write(" + tagvar + ".toString());");
                }
                code("} else {");
                code("pw.write(\"" + element.getTag() + "\");");
                code("}");
            }
        } else {
            print(element.getTag());
        }
    }

    private void genTagClose(PlainElement element, String tagvar, Expression tagexp) {
        print("</");
        writeTag(element, tagvar, tagexp);
        print(">");
    }

    private void genTagOpen(PlainElement element, String tagvar, Expression tagexp) {
        print("<");
        if (tagvar != null) {
            assignVar(tagexp, tagvar);
        }
        writeTag(element, tagvar, tagexp);

        class Wow {
            String val;
            Expression wi;
            String wo;
        }
        Map<String, Wow> attrs = new LinkedHashMap<String, Wow>();
        Set<String> ignored = new HashSet<String>();
        for (Map.Entry<String, String> entry : element.getAttributes().entrySet()) {
            String key = entry.getKey();
            if (key.startsWith(TN_Q) || key.startsWith(TN_W)) continue;
            Wow wow = new Wow();
            wow.val = entry.getValue();
            String s = element.getAttributes().get(TN_W + key);
            if (s != null) {
                wow.wo = s;
                ignored.add(TN_W + key);
            }
            s = element.getAttributes().get(TN_Q + key);
            if (s != null) {
                wow.wi = parseExpression(s);
                if (wow.wi != null) {
                    ignored.add(TN_Q + key);
                }
            }
            ignored.add(key);
            attrs.put(key, wow);
        }
        for (Map.Entry<String, String> entry : element.getAttributes().entrySet()) {
            String key = entry.getKey();
            if (!key.startsWith(TN_W) || key.equals(TN_W) || key.equals(TN_W + "content")) continue;
            if (ignored.contains(key)) continue;
            String name = key.substring(TN_W.length());
            Wow wow = new Wow();
            wow.wo = entry.getValue();
            String s = element.getAttributes().get(TN_Q + name);
            if (s != null) {
                wow.wi = parseExpression(s);
                if (wow.wi != null) {
                    ignored.add(TN_Q + name);
                }
            }
            ignored.add(key);
            attrs.put(name, wow);
        }
        for (Map.Entry<String, String> entry : element.getAttributes().entrySet()) {
            String key = entry.getKey();
            if (!key.startsWith(TN_Q) || key.endsWith(TN_Q + "content")) continue;
            if (ignored.contains(key)) continue;
            Expression exp = parseExpression(entry.getValue());
            if (exp != null) {
                Wow wow = new Wow();
                wow.wi = exp;
                String name = key.substring(TN_Q.length());
                wow.val = name;
                attrs.put(name, wow);
                ignored.add(key);
            }
        }
        for (Map.Entry<String, String> entry : element.getAttributes().entrySet()) {
            String key = entry.getKey();
            if (ignored.contains(key)) continue;
            if (key.equals(TN_W + "content")) continue;
            if (key.equals(TN_Q + "content")) {
                Expression exp = parseExpression(entry.getValue());
                if (exp != null) continue;
            }
            Wow wow = new Wow();
            wow.val = entry.getValue();
            attrs.put(key, wow);
        }
        for (Map.Entry<String, Wow> entry : attrs.entrySet()) {
            String key = entry.getKey();
            Wow wow = entry.getValue();

            if (wow.wi != null) {
                if (wow.wi.getEvalString().equals("false")) continue;
                if (!wow.wi.getEvalString().equals("true")) {
                    beforeIf(wow.wi);
                    print(" " + key + "=\"");
                    if (wow.wo != null) {
                        gen(wow.wo, true);
                    } else {
                        gen(wow.val, false);
                    }
                    print("\"");
                    code("}");
                    continue;
                }
            }
            print(" " + key + "=\"");
            if (wow.wo != null) {
                gen(wow.wo, true);
            } else {
                gen(wow.val, false);
            }
            print("\"");
        }
        if (isEmptyTag(element)) {
            print("/");
        }
        print(">");
    }

    private static Set<String> emptyTags;

    static {
        emptyTags = new HashSet<String>(
                Arrays.asList("area",
                        "base",
                        "basefont",
                        "br",
                        "col",
                        "frame",
                        "hr",
                        "img",
                        "input",
                        "isindex",
                        "link",
                        "meta",
                        "param")
        );
    }

    private boolean isEmptyTag(PlainElement element) {
        return emptyTags.contains(element.getTag().toLowerCase());
    }

    private String declareVar(Expression exp, String var) {
        if (var == null) var = "v" + seq++;
        code(exp.getReturnType() + " " + var + " = " + exp.getReturnType().getDefaultValue() + ";");
        assignVar(exp, var);
        return var;
    }

    private void assignVar(Expression exp, String var) {
        code("try {");
        code(var + " = " + exp.getEvalString() + ";");
        code("} catch(Exception e) {");
        if (RenderExceptionAware.class.isAssignableFrom(componentClass)) {
            code("b0.renderError(new " + RenderException.class.getName() + "(\"" + exp.getOriginalString() + "\", e));");
        } else {
            code("throw new " + RenderException.class.getName() + "(\"" + exp.getOriginalString() + "\", e);");
        }
        code("}");
    }

    private Expression parseExpression(String s) {
        s = s.trim();
        if (s.startsWith("${") && s.endsWith("}")) s = s.substring(2, s.length() - 1);
        ExpParser parser = new ExpParser(comContexts, varContexts);
        try {
            return parser.parse(s);
        } catch (ParseException e) {
            return null;
        }
    }

    private final static Pattern $pattern = Pattern.compile("\\$\\{.+?\\}");

    private String writeVar(Expression exp, String var) {
        if (var == null) {
            var = declareVar(exp, null);
        }
        if (exp.getReturnType().isPrimitive()) {
            code("pw.write(String.valueOf(" + var + "));");
        } else {
            code("if(" + var + "!=null) ");
            if (exp.getReturnType().toClass() == String.class) {
                code("pw.write(" + var + ");");
            } else {
                code("pw.write(" + var + ".toString());");
            }
        }
        return var;
    }

    private void gen(String text, boolean maybeExp) {
        if (maybeExp) {
            Expression exp = parseExpression("${" + text + "}");
            if (exp != null) {
                writeVar(exp, null);
                return;
            }
        }
        int idx = 0;
        Matcher matcher = $pattern.matcher(text);
        while (matcher.find()) {
            int s = matcher.start();
            int e = matcher.end();
            print(text.substring(idx, s));
            Expression exp = parseExpression(text.substring(s, e));
            if (exp == null) {
                print(text.substring(s, e));
            } else {
                writeVar(exp, null);
            }
            idx = e;
        }
        print(text.substring(idx, text.length()));
    }
}
